package jxtend.processor.eclipse.visitor;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jxtend.processor.eclipse.utils.ASTNodeUtils;
import jxtend.processor.eclipse.utils.EclipseProcessorUtils;
import jxtend.processor.eclipse.visitor.handler.ASTNodeHandlerVisitorContext;
import jxtend.processor.eclipse.visitor.handler.ProblemSeverity;
import jxtend.processor.eclipse.visitor.handler.impl.ASTNodeBits;
import jxtend.processor.eclipse.visitor.replace.ASTNodeReplaceHandler;
import jxtend.processor.exception.JxtendProcessorException;
import jxtend.processor.reflect.ReflectMethod;
import jxtend.processor.reflect.ReflectMethod.ResultMethod1Args;

import org.eclipse.jdt.core.compiler.CategorizedProblem;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.ASTVisitor;
import org.eclipse.jdt.internal.compiler.CompilationResult;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.AbstractVariableDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Expression;
import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration;
import org.eclipse.jdt.internal.compiler.ast.ImportReference;
import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Statement;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.impl.ReferenceContext;
import org.eclipse.jdt.internal.compiler.lookup.Binding;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.ClassScope;
import org.eclipse.jdt.internal.compiler.lookup.CompilationUnitScope;
import org.eclipse.jdt.internal.compiler.lookup.ExtraCompilerModifiers;
import org.eclipse.jdt.internal.compiler.lookup.FieldBinding;
import org.eclipse.jdt.internal.compiler.lookup.ImportBinding;
import org.eclipse.jdt.internal.compiler.lookup.LookupEnvironment;
import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.MethodScope;
import org.eclipse.jdt.internal.compiler.lookup.PackageBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.eclipse.jdt.internal.compiler.problem.DefaultProblem;
import org.eclipse.jdt.internal.compiler.util.HashtableOfObject;

@SuppressWarnings("restriction")
/* default */ class ASTNodeHandlerVisitorContextImpl implements ASTNodeHandlerVisitorContext {

	private static final ResultMethod1Args<MethodScope, MethodBinding, AbstractMethodDeclaration> createMethodMethod =
		ReflectMethod.method(MethodScope.class, MethodBinding.class, "createMethod", AbstractMethodDeclaration.class);
	private static final ResultMethod1Args<LookupEnvironment, PackageBinding, char[]> getTopLevelPackageMethod =
		ReflectMethod.method(LookupEnvironment.class, PackageBinding.class, "getTopLevelPackage", char[].class);
	
	private final CompilationUnitDeclaration compilationUnitDeclaration;
	/* default */ final List<ASTNodeReplaceHandler> astNodeReplaceHandlers;
	/* default */ final List<ASTNode> parentNodes;
	private final ASTVisitor undoTypeResolveASTVisitor;
	private final Set<ReferenceContext> referenceContextsWithIgnoreFurtherInvestigation;
	private final ASTVisitor markASTNodeAsBeenGeneratedASTVistor;

	private ASTNode currentNode;
	
	private ASTVisitor astVisitor;
	
	private Map<ASTNode, Integer> statementBits;

	public ASTNodeHandlerVisitorContextImpl(List<ASTNode> parentNodes,
			CompilationUnitDeclaration compilationUnitDeclaration, List<ASTNodeReplaceHandler> astNodeReplaceHandlers,
			ASTVisitor undoTypeResolveASTVisitor, 
			Set<ReferenceContext> referenceContextsWithIgnoreFurtherInvestigation) {
		this.parentNodes = parentNodes;
		this.compilationUnitDeclaration = compilationUnitDeclaration;
		this.astNodeReplaceHandlers = astNodeReplaceHandlers;
		this.undoTypeResolveASTVisitor = undoTypeResolveASTVisitor;
		this.referenceContextsWithIgnoreFurtherInvestigation = referenceContextsWithIgnoreFurtherInvestigation;
		this.markASTNodeAsBeenGeneratedASTVistor =
			ASTVisitorFactory.create(new MarkAsGeneratedASTNodeVisitor(), parentNodes);
	}
	
	public ASTVisitor getAstVisitor() {
		return astVisitor;
	}

	public void setAstVisitor(ASTVisitor astVisitor) {
		this.astVisitor = astVisitor;
	}

	public void setStatementBits(Map<ASTNode, Integer> statementBits) {
		this.statementBits = statementBits;
	}

	@Override
	public ASTNode getParentNode() {
		return !parentNodes.isEmpty() ? parentNodes.get(parentNodes.size() - 1) : null;
	}

	@Override
	public List<ASTNode> getParentNodes() {
		List<ASTNode> result = new ArrayList<ASTNode>(parentNodes);
		Collections.reverse(result);
		return result;
	}

	@Override
	public ASTNode getParentNode(int index) {
		if (index >= parentNodes.size()) {
			//TODO: Fill in exception message
			throw new IndexOutOfBoundsException();
		}

		return parentNodes.get(parentNodes.size() - index - 1);
	}

	@Override
	public <A extends ASTNode> A findParentNode(Class<A> parentNodeType) {
		for (int i = parentNodes.size() - 1; i >= 0; i--) {
			ASTNode currentNode = parentNodes.get(i);
			
			if (parentNodeType.isAssignableFrom(ASTNodeUtils.getClassForNormalKindASTNode(currentNode))) {
				@SuppressWarnings("unchecked") A result = (A) currentNode; 
				return result;
			}
		}

		return null;
	}

	@Override
	public ReferenceContext getCurrentReferenceContext() {
		for (int i = parentNodes.size() - 1; i >= 0; i--) {
			ASTNode currentNode = parentNodes.get(i);

			if (currentNode instanceof ReferenceContext) {
				return (ReferenceContext) currentNode;
			}
		}

		throw new IllegalStateException("Couldn't determine the current reference context.");
	}

	public void pushNode(ASTNode node) {
		parentNodes.add(node);
	}

	public void popNode(ASTNode node) {
		parentNodes.remove(node);
	}

	@Override
	public void backupStatementBits(Statement statement) {
		statementBits.put(statement, statement.bits);
	}

	@Override
	public void appendMethod(TypeDeclaration typeDeclaration, MethodDeclaration methodDeclaration) {
		methodDeclaration.traverse(markASTNodeAsBeenGeneratedASTVistor, typeDeclaration.scope);

	    typeDeclaration.methods = EclipseProcessorUtils.appendToArray(
	    		typeDeclaration.methods, AbstractMethodDeclaration.class, methodDeclaration);

		MethodScope scope = new MethodScope(typeDeclaration.scope, methodDeclaration, false);
		MethodBinding methodBinding = createMethodMethod.invoke(scope, methodDeclaration);
//		typeDeclaration.binding.resolveTypesFor(methodBinding);

//		methodBinding.modifiers |= ExtraCompilerModifiers.AccUnresolved;

//		if (methodDeclaration.returnType != null) {
//			methodDeclaration.returnType.resolve(scope);
//
//			methodBinding.returnType = methodDeclaration.returnType.resolvedType;
//		}

		methodDeclaration.binding = methodBinding;
	
		if ((methodDeclaration.arguments != null) && (methodDeclaration.arguments.length != 0)) {
			methodDeclaration.binding.parameters = new TypeBinding[methodDeclaration.arguments.length];

			for (int i = 0; i < methodDeclaration.arguments.length; i++) {
				TypeBinding argumentTypeBinding = (methodDeclaration.arguments[i].binding != null)
					? methodDeclaration.arguments[i].binding.type : null;

				if (argumentTypeBinding == null) {
					argumentTypeBinding = methodDeclaration.arguments[i].type.resolveType(typeDeclaration.scope);
				}

				methodDeclaration.binding.parameters[i] = argumentTypeBinding;
			}
		}

		MethodBinding[] currentMethodBindingArray = typeDeclaration.binding.methods();
		MethodBinding[] newMethodBindingArray = new MethodBinding[currentMethodBindingArray.length + 1];
		System.arraycopy(currentMethodBindingArray, 0, newMethodBindingArray, 0, currentMethodBindingArray.length);
//		MethodBinding newMethodBinding = new MethodBinding(
//				methodDeclaration.modifiers, methodDeclaration.selector,
//				methodDeclaration.returnType.resolveType(methodDeclaration.scope),
//				argumentTypeBindings(methodDeclaration), Binding.NO_EXCEPTIONS,
//				typeDeclaration.binding);
		newMethodBindingArray[newMethodBindingArray.length - 1] = methodBinding;

		Annotation[] annotations = methodDeclaration.annotations;

//		List<Annotation> resolvedAnnotationList = new ArrayList<Annotation>();
//		if ((annotations != null) && (annotations.length != 0)) {
//			for (int i = 0; i < annotations.length; i++) {
//				if ((annotations[i].resolvedType == null)
//						|| !EclipseProcessorUtils.isValidTypeReference(annotations[i].resolvedType)
//						|| (annotations[i].resolvedType instanceof ParameterizedTypeBinding)) {
//					recordProblemForNode(ProblemSeverity.ERROR, typeDeclaration,
//							annotations[i].sourceStart, annotations[i].sourceEnd,
//							"Annotation could not be resolved");
//
//					continue;
//				}
//
//				resolvedAnnotationList.add(annotations[i]);
//			}
//			
//			AnnotationBinding[] annotationBindings = new AnnotationBinding[resolvedAnnotationList.size()];
//			for (int i = 0; i < resolvedAnnotationList.size(); i++) {
//				Annotation resolvedAnnotation = resolvedAnnotationList.get(i);
//
//				annotationBindings[i] = getLookupEnvironment().createAnnotation(
//						(ReferenceBinding) resolvedAnnotation.resolvedType,
//						resolvedAnnotation.computeElementValuePairs());				
//			}
//			methodBinding.setAnnotations(
//					annotationBindings, (AnnotationBinding[][]) null, null, getLookupEnvironment());
//		}
		
		Arrays.sort(newMethodBindingArray, new Comparator<MethodBinding>() {
			@Override
			public int compare(MethodBinding o1, MethodBinding o2) {
				return new String(o1.selector).compareTo(new String(o2.selector));
			}
		});
		
		typeDeclaration.binding.setMethods(newMethodBindingArray);
	}

	private static TypeBinding[] argumentTypeBindings(MethodDeclaration methodDeclaration) {
		if ((methodDeclaration.arguments == null) || (methodDeclaration.arguments.length == 0)) {
			return Binding.NO_PARAMETERS;
		}

		TypeBinding[] result = new TypeBinding[methodDeclaration.arguments.length];

		int index = 0;
		for (Argument argument : methodDeclaration.arguments) {
			result[index] = argument.type.resolveType(methodDeclaration.scope);

			index++;
		}

		return result;
	}

	@Override
	public void recordProblemForNode(ProblemSeverity severity, ReferenceContext referenceContext, int startPosition, int endPosition, String message) {
		CompilationResult compilationResult = compilationUnitDeclaration.compilationResult;

		int severityFlags = (severity != null) ? severity.getSeverityFlags() : 0;

		CategorizedProblem problem = new DefaultProblem(compilationResult.fileName, message, -1, new String[0],
				severityFlags, startPosition, endPosition, 0, 0);

		compilationUnitDeclaration.problemReporter.record(
				problem, compilationUnitDeclaration.compilationResult, referenceContext);

		if (severity == ProblemSeverity.ERROR) {
			referenceContextsWithIgnoreFurtherInvestigation.add(referenceContext);
		}
	}

	@Override
	public LookupEnvironment getLookupEnvironment() {
		return compilationUnitDeclaration.scope.environment;
	}

	@Override
	public ImportReference addImport(char[][] tokens) {
		ImportReference[] oldImports = (ImportReference[]) compilationUnitDeclaration.imports;
		if (oldImports != null) {
			for (ImportReference importReference : oldImports) {
				if (!importReference.isStatic() && ((importReference.bits & ASTNode.OnDemand) == 0)
						&& (CharOperation.equals(tokens, importReference.tokens))) {
					return importReference;
				}
			}
		}

		int oldImportsLength = (oldImports != null) ? oldImports.length : 0;

		ImportReference[] newImports = new ImportReference[oldImportsLength + 1];
		if (oldImportsLength != 0) {
			System.arraycopy(oldImports, 0, newImports, 0, oldImportsLength);
		}

//		ImportReference packageDeclaration = compilationUnitDeclaration.currentPackage;
//
//		int importSourceStart = (packageDeclaration != null) ? packageDeclaration.sourceEnd + 4 : 0;
//		int importSourceEnd = (packageDeclaration != null) ? packageDeclaration.sourceEnd + 4 : 0;
		long[] sourcePositions = ASTNodeUtils.encodePosArray(tokens.length, 0, 0);

		ImportReference importReference = new ImportReference(tokens, sourcePositions, false, 0);
		importReference.bits |= ASTNodeBits.JXTEND_GENERATED;
//		importReference.declarationSourceStart = importSourceStart;
//		importReference.declarationSourceEnd = importSourceEnd;
//		importReference.declarationEnd = importSourceEnd;

		newImports[newImports.length - 1] = importReference;

		compilationUnitDeclaration.imports = newImports;

		Binding importBinding = null;

		if (tokens.length > 0) {
			importBinding = getTopLevelPackageMethod.invoke(getLookupEnvironment(), tokens[0]);

			for (int i = 1; i < tokens.length; i++) {
				if (importBinding == null) { break; }

				importBinding = ((PackageBinding) importBinding).getTypeOrPackage(tokens[i]);
			}
		}

		if (importBinding != null && !importBinding.isValidBinding()) {
			throw new JxtendProcessorException("No valid binding found for import");
		}
		
		ImportBinding[] oldBindings = (ImportBinding[]) compilationUnitDeclaration.scope.imports;
		int oldBindingsLength = (oldBindings != null) ? oldBindings.length : 0;

		ImportBinding[] newBindings = new ImportBinding[oldBindingsLength + 1];
		if (oldBindingsLength != 0) {
			System.arraycopy(oldBindings, 0, newBindings, 0, oldBindingsLength);
		}

		ImportBinding newImportBinding = new ImportBinding(tokens, false, importBinding, importReference);
		newBindings[newBindings.length - 1] = newImportBinding;

		compilationUnitDeclaration.scope.imports = newBindings;

		addToTypeOrPackageCache(compilationUnitDeclaration.scope, newImportBinding);

		return importReference;
	}

	public static void addToTypeOrPackageCache(CompilationUnitScope scope, ImportBinding importBinding) {
		ImportBinding[] imports = scope.imports;

		if ((imports == null) || (imports.length == 0)) { return; }

		boolean importBindingMightAlreadyExist = true;
		HashtableOfObject typeOrPackageCache = scope.typeOrPackageCache;
		if (typeOrPackageCache == null) {
			return;
		}

		char[] lastCompoundNamePart = importBinding.compoundName[importBinding.compoundName.length - 1];

		if (importBindingMightAlreadyExist && typeOrPackageCache.containsKey(lastCompoundNamePart)) { return; }

		typeOrPackageCache.put(lastCompoundNamePart, importBinding);
	}

	@Override
	public void appendField(TypeDeclaration typeDeclaration, FieldDeclaration fieldDeclaration) {
		fieldDeclaration.traverse(markASTNodeAsBeenGeneratedASTVistor, typeDeclaration.initializerScope);

		FieldBinding fieldBinding = new FieldBinding(fieldDeclaration, (TypeBinding) null,
				fieldDeclaration.modifiers | ExtraCompilerModifiers.AccUnresolved, typeDeclaration.binding);
		fieldDeclaration.binding = fieldBinding;
		
//		if (methodDeclaration.returnType != null) {
//			methodDeclaration.returnType.resolve(scope);
//
//			methodBinding.returnType = methodDeclaration.returnType.resolvedType;
//		}
//
//		methodDeclaration.binding = methodBinding;
		
		typeDeclaration.fields =
			EclipseProcessorUtils.appendToArray(typeDeclaration.fields, FieldDeclaration.class, fieldDeclaration);
	}

	@Override
	public void prependStatement(AbstractMethodDeclaration abstractMethodDeclaration, Statement statement) {
		statement.traverse(markASTNodeAsBeenGeneratedASTVistor, abstractMethodDeclaration.scope);

		Statement[] oldStatements = abstractMethodDeclaration.statements;
		int oldStatementsLength = (oldStatements != null) ? oldStatements.length : 0;

		Statement[] newStatements = new Statement[oldStatementsLength + 1];
		int statementIndex = 0;
		if (oldStatementsLength > 0) {
			for (Statement oldStatement : oldStatements) {
				if ((oldStatement.bits & ASTNodeBits.JXTEND_GENERATED) != 0) {
					statementIndex++;
				} else {
					break;
				}
			}

			if (statementIndex > 0) {
				System.arraycopy(oldStatements, 0, newStatements, 0, statementIndex);
			}
			System.arraycopy(oldStatements, statementIndex, newStatements, statementIndex + 1,
					oldStatementsLength - statementIndex);
		}

		newStatements[statementIndex] = statement;
		abstractMethodDeclaration.statements = newStatements;
	}

	@Override
	public void replaceNode(ASTNode newNode) {
		boolean replaced = false;
		for (ASTNodeReplaceHandler replaceHandler : astNodeReplaceHandlers) {
			replaced = replaceHandler.replaceNode(getParentNode(), currentNode, newNode);

			if (replaced) {
				currentNode = newNode;
				break;
			}
		}

		if (!replaced) {
			throw new JxtendProcessorException("Could not find handler to replace the node");
		}
	}

	/* default */ ASTNode getCurrentNode() {
		return currentNode;
	}

	/* default */ void setCurrentNode(ASTNode currentNode) {
		this.currentNode = currentNode;
	}

	@Override
	public void resolveExpression(Expression expression, BlockScope blockScope) {
		expression.traverse(undoTypeResolveASTVisitor, blockScope);
		statementBits.put(expression, expression.bits);

		expression.resolve(blockScope);
	}

	@Override
	public void resolveExpression(Expression expression, ClassScope classScope) {
		expression.traverse(undoTypeResolveASTVisitor, classScope);
		statementBits.put(expression, expression.bits);

		expression.resolveType(classScope);
	}

	@Override
	public void resolveVariableDeclaration(AbstractVariableDeclaration variableDeclaration, BlockScope blockScope) {
		if ((blockScope instanceof MethodScope) && (variableDeclaration instanceof FieldDeclaration)) {
		variableDeclaration.traverse(undoTypeResolveASTVisitor, blockScope);
		statementBits.put(variableDeclaration, variableDeclaration.bits);

		((FieldDeclaration) variableDeclaration).resolve((MethodScope) blockScope);
		}
	}

}
